本系列文章會在筆者的部落格繼續連載!Design System 101 感謝大家的閱讀!
深淺主題已經在前端社群風靡多年,小至個人部落格大至世界級客戶端應用 (Github),都有深淺色的切換功能,提供使用者可以自由地切換主題。
而要讓一個網站可以無痛的切換深淺色,最主要的原則就是在開發時遵循 Design Token,在先前 Token to CSS 的章節提到過其好處,如果色票沒有列在 Token 內則需要在應用層的 CSS 中去處理。
本篇會介紹如何透過 useTheme 去控制應用的深淺主題。
在最初的一開始我們介紹 Token 這個概念,透過 System Token (alias.json) 的 light 與 dark 分別指向不同的 Ref Token (base.json),這一步讓我們有能力可以讓同一個 System Token 的 alias 根據某些條件轉換成不同的值。
接著再透過 Design Token 建置時,將 :root 拆分成 light 與 dark 到 normalized.css,並在網頁一開始載入該 CSS ,最後透過 data-theme 去控制要使用哪種主題色。
html[data-theme='light'],
.tocino-light {
  --tocino-sys-color-primary: #6750a4;
  // ... All System Token (淺色)
}
:root {
  --tocino-sys-color-primary: #d0bcff;
  // ... All System Token (深色)
}
接下來要實作的就是如何提供一個功能讓使用者可以切換 data-theme 的值,也就是本篇要介紹的 useTheme。
useTheme 要做的事情很簡單,就是控制 data-theme 的值。
理想上我們希望當使用 useTheme 時可以讓它可以讓我們更新 data-theme 以及知道當前的 theme
import { useTheme } from '@tocino-ui/core/hooks';
export default () => {
  const { theme, toggleTheme } = useTheme();
  return (
    <div>
      <span>Current Theme: {theme}</span>
      <button onClick={toggleTheme}>Toggle Theme</button>
    </div>
  );
};
| 名稱 | 型別 | 初始值 | 描述 | 
|---|---|---|---|
| initialTheme | string | - | 初始的主題 | 
| 名稱 | 型別 | 初始值 | 描述 | 
|---|---|---|---|
| theme | string | dark | 當前的主題 | 
| toggleTheme | () => void | - | 切換主題 | 
| setTheme | React.SetStateAction<string> | - | 設定主題 | 
首先是 useTheme 的實作,會先透過 getTheme 取得 localStorage 的上次存取的 Theme, 如果沒有值,就將深色主題作為預設值,並且 mounting 的時候透過 useEffect 中去更新 data-theme 的值。最後將 theme, toggleTheme 與 setTheme 這三個 API 透過 useMemo 傳遞給使用者。
而 themeManager 主要負責 localStorage 的存取與透過 documentElement.setAttribute 去更新 data-theme 的值。
/**
 * ====== CONST ======
 */
const THEME_KEY = 'tocino-theme';
const THEME = {
  LIGHT: 'light',
  DARK: 'dark',
  SYSTEM: 'system',
};
/**
 * ====== THEME MANAGER ======
 */
const themeManager = {
  getTheme: () => {
    return window?.localStorage.getItem(THEME_KEY) ?? THEME.DARK;
  },
  setTheme: (theme) => {
    document?.documentElement.setAttribute('data-theme', theme);
    window?.localStorage.setItem(THEME_KEY, theme);
  },
};
/**
 * ====== `useTheme` ======
 */
function useTheme(initialTheme) {
  const [theme, setTheme] = useState(() => {
    return initialTheme ?? themeManager.getTheme(); // 取得 Default Theme
  });
  useEffect(() => {
    themeManager.setTheme(theme);
  }, [theme]);
  const toggleTheme = useCallback((theme) => {
    setTheme((prev) => (prev === THEME.DARK ? THEME.LIGHT : THEME.DARK));
  }, []);
  return useMemo(
    () => ({
      theme,
      toggleTheme,
      setTheme,
    }),
    [theme, toggleTheme],
  );
}

可以看到上面引入了我們先前做的 Button 組件,並且透過 useTheme 去控制 data-theme 的值,這樣就可以讓我們的應用自由地切換深淺色主題。
明天見!